#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include "backend_list.h"
#include "http_server.h"
#include "log.h"

#define BACKEND_ALLOC               10
#define MIN_SECONDS_BETWEEN_RETRIES 60

static int allocate (backend_list_t *backend_list);
static int insert (backend_list_t *backend_list, const char *host, uint32_t ip, uint16_t port);

void backend_list_init (backend_list_t *backend_list)
{
	backend_list->backends = NULL;
	backend_list->size = 0;
	backend_list->used = 0;

	backend_list->current = 0;
}

void backend_list_free (backend_list_t *backend_list)
{
	size_t i;

	if (backend_list->backends) {
		for (i = 0; i < backend_list->used; i++) {
			buffer_free (&(backend_list->backends[i].host));
		}

		free (backend_list->backends);
		backend_list->backends = NULL;
	}

	backend_list->size = 0;
	backend_list->used = 0;

	backend_list->current = 0;
}

int allocate (backend_list_t *backend_list)
{
	backend_t *backends;
	size_t size;

	if (backend_list->used == backend_list->size) {
		size = backend_list->size + BACKEND_ALLOC;
		backends = (backend_t *) realloc (backend_list->backends, size * sizeof (backend_t));
		if (!backends) {
			return -1;
		}

		backend_list->backends = backends;
		backend_list->size = size;
	}

	return 0;
}

int insert (backend_list_t *backend_list, const char *host, uint32_t ip, uint16_t port)
{
	backend_t *backend;

	if (allocate (backend_list) < 0) {
		return -1;
	}

	backend = &(backend_list->backends[backend_list->used]);

	buffer_init (&backend->host, 32);
	if (buffer_append_string (&backend->host, host) < 0) {
		return -1;
	}

	backend->addr.sin_family = AF_INET;
	backend->addr.sin_port = htons (port);
	backend->addr.sin_addr.s_addr = ip;
	memset (&(backend->addr.sin_zero), 0, 8);

	backend->last_connection = 0;
	backend->weight = 0;

	backend_list->used++;

	return 0;
}

int backend_list_load (backend_list_t *backend_list, configuration_t *conf, const char *host_name, const char *rule_name)
{
	struct in_addr addr;
	const char *string;
	const char *ptr;
	char ip[16];
	int port;
	size_t len;
	size_t i;

	/* Free previous configuration. */
	backend_list_free (backend_list);

	for (i = 0; ((string = configuration_get_child (conf, i, "VirtualHosts", host_name, "RequestHandling", rule_name, "Backends", NULL)) != NULL); i++) {
		ptr = strchr (string, ':');
		if (ptr) {
			len = ptr - string;

			port = atoi (ptr + 1);
			if ((port < 1) || (port > 65535)) {
				fprintf (stderr, "Ignoring backend: [%s].\n", string);
				continue;
			}
		} else {
			len = strlen (string);

			port = 80;
		}

		if ((len < 7) || (len > 15)) {
			fprintf (stderr, "Ignoring backend: [%s].\n", string);
			continue;
		}

		memcpy (ip, string, len);
		ip[len] = 0;
		if (inet_aton (ip, &addr) < 0) {
			fprintf (stderr, "Ignoring backend: [%s].\n", string);
			continue;
		}

		if (insert (backend_list, string, addr.s_addr, (uint16_t) port) < 0) {
			/* Couldn't allocate memory. */
			fprintf (stderr, "[backend_list_load] Couldn't allocate memory.\n");
			return -1;
		}
	}

	if (backend_list->used == 0) {
		return -1;
	}

	return 0;
}

int backend_list_connect (backend_list_t *backend_list, http_connection_t *http_connection)
{
	backend_t *backend;
	size_t current;
	int sd;
	int flags;

	/* Create socket. */
	sd = socket (PF_INET, SOCK_STREAM, 0);
	if (sd < 0) {
		if (http_server.log_fd != -1) {
			log_error ("socket: %s.", strerror (errno));
		}

		return -1;
	}

	/* Make socket non-blocking. */
	flags = fcntl (sd, F_GETFL);
	flags |= O_NONBLOCK;
	if (fcntl (sd, F_SETFL, flags) < 0) {
		if (http_server.log_fd != -1) {
			log_error ("fcntl: %s.", strerror (errno));
		}

		close (sd);
		return -1;
	}

	/* Disable the Nagle algorithm. */
	flags = 1;
	setsockopt (sd, IPPROTO_TCP, TCP_NODELAY, (const char *) &flags, sizeof (flags));

	/* Connect. */
	current = backend_list->current;

	do {
		backend = &(backend_list->backends[backend_list->current]);
		if ((backend->weight != -1) || (http_server.current_time - backend->last_connection >= MIN_SECONDS_BETWEEN_RETRIES)) {
			if ((connect (sd, (struct sockaddr *) &backend->addr, sizeof (struct sockaddr)) < 0) && (errno != EINPROGRESS)) {
				if (http_server.log_fd != -1) {
					log_error ("Couldn't connect to backend: [%.*s].", backend->host.used, backend->host.data);
				}

				backend->last_connection = http_server.current_time;
				backend->weight = -1;

				backend_list->current = (backend_list->current + 1) % backend_list->used;

				continue;
			}

			backend->last_connection = http_server.current_time;
			backend->weight++;

			http_connection->connection_attempts++;
			http_connection->backend = backend_list->current;

			backend_list->current = (backend_list->current + 1) % backend_list->used;

			if (http_server_create_backend_connection (&http_server, sd, http_connection->sd) < 0) {
				break;
			}

			http_connection->fd = sd;

			return 0;
		}

		backend_list->current = (backend_list->current + 1) % backend_list->used;
	} while (current != backend_list->current);

	close (sd);

	return -1;
}

int backend_list_fcgi_connect (backend_list_t *backend_list, http_connection_t *http_connection)
{
	backend_t *backend;
	size_t current;
	int sd;
	int flags;

	/* Create socket. */
	sd = socket (PF_INET, SOCK_STREAM, 0);
	if (sd < 0) {
		if (http_server.log_fd != -1) {
			log_error ("socket: %s.", strerror (errno));
		}

		return -1;
	}

	/* Make socket non-blocking. */
	flags = fcntl (sd, F_GETFL);
	flags |= O_NONBLOCK;
	if (fcntl (sd, F_SETFL, flags) < 0) {
		if (http_server.log_fd != -1) {
			log_error ("fcntl: %s.", strerror (errno));
		}

		close (sd);
		return -1;
	}

	/* Disable the Nagle algorithm. */
	flags = 1;
	setsockopt (sd, IPPROTO_TCP, TCP_NODELAY, (const char *) &flags, sizeof (flags));

	/* Connect. */
	current = backend_list->current;

	do {
		backend = &(backend_list->backends[backend_list->current]);
		if ((backend->weight != -1) || (http_server.current_time - backend->last_connection >= MIN_SECONDS_BETWEEN_RETRIES)) {
			if ((connect (sd, (struct sockaddr *) &backend->addr, sizeof (struct sockaddr)) < 0) && (errno != EINPROGRESS)) {
				if (http_server.log_fd != -1) {
					log_error ("Couldn't connect to FastCGI server: [%.*s].", backend->host.used, backend->host.data);
				}

				backend->last_connection = http_server.current_time;
				backend->weight = -1;

				backend_list->current = (backend_list->current + 1) % backend_list->used;

				continue;
			}

			backend->last_connection = http_server.current_time;
			backend->weight++;

			http_connection->connection_attempts++;
			http_connection->backend = backend_list->current;

			backend_list->current = (backend_list->current + 1) % backend_list->used;

			if (http_server_create_fcgi_connection (&http_server, sd, http_connection->sd) < 0) {
				break;
			}

			http_connection->fd = sd;

			return 0;
		}

		backend_list->current = (backend_list->current + 1) % backend_list->used;
	} while (current != backend_list->current);

	close (sd);

	return -1;
}
